# -*- coding: utf-8 -*-
# @Author: Jerry
# @Date:   2022-02-25 10:44:47
# @Last Modified by:   Jerry
# @Last Modified time: 2022-03-01 15:30:09

# **********
# **********
# OpenCV中的图像处理 » 4_9_2_轮廓特征
# http://www.woshicver.com

# **********
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
import os
rootpath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
datapath = os.path.join(rootpath,'data')
imgpath = lambda name: os.path.join(datapath,name)


# **********
'''目标
在本文中，我们将学习
- 如何找到轮廓的不同特征，例如面积，周长，质心，边界框等。
- 您将看到大量与轮廓有关的功能。'''

# **********
'''1. 特征矩
特征矩可以帮助您计算一些特征，例如物体的质心，物体的面积等。
请查看特征矩上的维基百科页面。函数cv.moments()提供了所有计算出的矩值的字典。'''

img = cv.imread(imgpath('star.png'))
imggray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
ret,thresh = cv.threshold(imggray,127,255,0)
contours,hierarchy = cv.findContours(thresh,1,2)
cnt = contours[0]
M = cv.moments(cnt)
print M
'''M的数据内容如下:
{
    'nu02': 0.058548033138277354,
    'nu03': 0.0007249156384667961,
    'nu11': 0.0024757791494012292,
    'nu12': 0.000136358099083374,
    'nu20': 0.1353930790132769,
    'nu21': 0.0012679003874905166,
    'nu30': -0.0009704328394501531,
    'm00': 22573.0,
    'm01': 1647321.0,
    'm02': 150049944.5,
    'm03': 15359979504.7,
    'm10': 2403461.1666666665,
    'm11': 176660085.16666666,
    'm12': 16171136899.383333,
    'm20': 324896867.3333333,
    'm21': 24075862143.983334
    'm30': 49210167294.65,
    'mu02': 29832584.067580745,
    'mu03': 55495871.099378586,
    'mu11': 1261509.3223172426,
    'mu12': 10438885.697227001,
    'mu20': 68988234.02474812,
    'mu21': 97064034.40246487,
    'mu30': -74291424.97550964,
}'''

# **********
# 获取到M值后，可以用M值提取有用的数据，例如面积，质心等
# # 质心关系如下：
# Cx = m10/m00 和 Cy = m01/m00
cx = int(M['m10']/M['m00'])
cy = int(M['m01']/M['m00'])
print 'cx =',cx,'cy =',cy

# **********
# 2.轮廓面积
# 轮廓区域由函数 cv.contourArea()或从矩M['m00']中给出。
area = cv.contourArea(cnt)
print 'area =',area


# **********
# 3. 轮廓周长
# 也称为弧长。可以使用 cv.arcLength() 函数找到它。
# 第二个参数指定形状是闭合轮廓(True)还是曲线。
perimeter = cv.arcLength(cnt,True)
print 'arc Length =',perimeter

# **********
# 4. 轮廓近似
# 根据我们指定的精度，它可以将轮廓形状近似为顶点数量较少的其他形状。
# 它是Douglas-Peucker算法的实现。检查维基百科页面上的算法和演示。
epsilon = 0.1*cv.arcLength(cnt,True)
approx = cv.approxPolyDP(cnt,epsilon,True) #这个True和上面的True一样，都是表示闭曲线
# 第二个参数称为epsilon，它是从轮廓到近似轮廓的最大距离。它是一个精度参数。
# 需要正确选择epsilon才能获得正确的输出。
img0 = img.copy()
cv.polylines(img0,[approx],True,(0,255,0),2)
# approx得到的仅仅是几个点集，需要将其连接起来
# 这个画线段的函数才能画出轮廓，中间的True也是指示是否为闭曲线的
# 而且画图的函数，都是直接改变src原图的，并不会再返回来一个画好的结果图
cv.imshow('img0',img0)
# 当epsilon的系数为0.01时, 描绘的形状将不成为矩形
epsilon1 = 0.01*cv.arcLength(cnt,True)
approx1 = cv.approxPolyDP(cnt,epsilon1,True)
img1 = img.copy()
cv.polylines(img1,[approx1],True,(0,255,0),2)
cv.imshow('img1',img1)


# **********
'''5.凸包
https://www.cnblogs.com/jclian91/p/9728488.html
凸包(Convex Hull)是一个计算几何（图形学）中的概念，
它的严格的数学定义为：在一个向量空间V中，对于给定集合X，
所有包含X的凸集的交集S被称为X的凸包。

在图像处理过程中，我们常常需要寻找图像中包围某个物体的凸包。
凸包跟多边形逼近很像，只不过它是包围物体最外层的一个凸集，这个凸集是所有能包围这个物体的凸集的交集。

凸包与轮廓近似相似，但不同，虽然有些情况下它们给出的结果是一样的。
函数cv2.convexHull() 可以用来检测一个曲线是否具有凸性缺陷，并能纠正缺陷。
一般而言，凸曲线是始终凸出或至少平坦的曲线。如果在内部凸出，则称为凸度缺陷。

cv.convexHull(points[, hull[, clockwise[, returnPoints]]]) -> hull
@param points Input 2D point set, stored in std::vector or Mat.
 是我们要传入的轮廓
@param hull Output convex hull.
 输出，通常不需要
@param clockwise Orientation flag. true/false
 方向标志，如果设置为True，输出的凸包是顺时针方向的，否则为逆时针方向
@param returnPoints Operation flag. true/false
 默认为True。它会返回凸包上点的坐标。如果设置为False，则会返回凸包点对应于原轮廓上的点坐标
'''
# 从以上解释和实例可知，凸包是对侦测物体区域面积的次优解，特别是对于mask等标示而言
# 在某些情况下，比如外形差别比较大的时候，可以使用此方法方便识别到状况
# 紧紧靠外形来识别的，肯定有一定的局限性

src = cv.imread(imgpath('hand.png'))
gray = cv.cvtColor(src,cv.COLOR_BGR2GRAY)
ret,thresh = cv.threshold(gray,127,255,0)
contours,hierarchy = cv.findContours(thresh,1,2)
cnt = contours[0]
hull = cv.convexHull(cnt) # returnPoints 默认为True。它会返回凸包上点的坐标。
cv.polylines(src,[hull],True,(0,0,255),2) #这个函数才能画出轮廓，中间的True也是指示是否为闭曲线的
cv.imshow('src',src)

# 寻找到图形中的ok手势，并标注出来
# https://www.cnblogs.com/jclian91/p/9728488.html
def convexHull_handok():
    src = cv.imread(imgpath('hand_ok.jpg'))
    gray = cv.cvtColor(src,cv.COLOR_BGR2GRAY)
    # 二值化，取阈值为235
    ret,thresh = cv.threshold(gray,235,255,cv.THRESH_BINARY)
    # 寻找图像中的轮廓
    contours, hierarchy = cv.findContours(thresh, 2, 1)
    # 寻找物体的凸包并绘制凸包的轮廓
    for cnt in contours:
        hull = cv.convexHull(cnt)
        length = len(hull)
        # 如果凸包点集中的点个数大于5
        if length>5:
            # 绘制图像凸包的轮廓
            for i in range(length):
                cv.line(src, tuple(hull[i][0]), tuple(hull[(i+1)%length][0]), (0,0,255),2)
    cv.imshow('ok',src)

convexHull_handok()

# **********
'''6. 检查凸度
cv.isContourConvex()具有检查曲线是否凸出的功能。
它只是返回True还是False。没什么大不了的。'''
k = cv.isContourConvex(cnt)
print 'k =',k # for hand pic, it's False, why?


# **********
'''7. 边界矩形
有两种类型的边界矩形。'''

# 7.a.直角矩形
# 它是一个矩形，不考虑物体的旋转。所以边界矩形的面积不是最小的。
# 它是由函数 cv.boundingRect() 找到的。
# 令(x，y)为矩形的左上角坐标，而(w，h)为矩形的宽度和高度。
lightsrc = cv.imread(imgpath('lighting1.png'))
lightgray = cv.cvtColor(lightsrc,cv.COLOR_BGR2GRAY)
ret,thresh = cv.threshold(lightgray,127,255,0)
contours,hierarchy = cv.findContours(thresh,1,2)
cnt = contours[0]
x,y,w,h = cv.boundingRect(cnt)
lightsrc1 = lightsrc.copy()
cv.rectangle(lightsrc1,(x,y),(x+w,y+h),(0,255,0),2)
cv.imshow('lightsrc_rectangle',lightsrc1)

# 7.b. 旋转矩形
# 这里，边界矩形是用最小面积绘制的，所以它也考虑了旋转。
# 使用的函数是 cv.minAreaRect()。
# 它返回一个Box2D结构，其中包含以下细节
# -(中心(x,y)，(宽度，高度)，旋转角度)。
# 但要画出这个矩形，我们需要矩形的四个角。
# 它由函数 cv.boxPoints ()获得
rect = cv.minAreaRect(cnt)
box = cv.boxPoints(rect)
box = np.int0(box)
lightsrc2 = lightsrc.copy()
cv.drawContours(lightsrc2,[box],0,(0,0,255),2)
cv.imshow('lightsrc_box',lightsrc2)

# **********
'''8. 最小闭合圈
使用函数 cv.minEnclosingCircle(cnt)查找对象的圆周。它是一个以最小面积完全覆盖物体的圆。'''
(x,y),radius = cv.minEnclosingCircle(cnt)
center = (int(x),int(y))
radius = int(radius)
lightsrc3 = lightsrc.copy()
cv.circle(lightsrc3,center,radius,(0,255,0),2)
cv.imshow('lightsrc_circle',lightsrc3)

# **********
'''9. 拟合一个椭圆
把一个椭圆拟合到一个物体上。它返回内接椭圆的旋转矩形。'''
ellipse = cv.fitEllipse(cnt)
lightsrc4 = lightsrc.copy()
cv.ellipse(lightsrc4,ellipse,(0,255,0),2)
cv.imshow('lightsrc_ellipse',lightsrc4)

# **********
'''10. 拟合直线
可以将一条直线拟合到一组点。下图包含一组白点。我们可以近似一条直线。'''
rows,cols = lightsrc.shape[:2]
[vx,vy,x,y] = cv.fitLine(cnt, cv.DIST_L2,0,0.01,0.01)
lefty = int((-x*vy/vx) + y)
righty = int(((cols-x)*vy/vx)+y)
lightsrc5 = lightsrc.copy()
cv.line(lightsrc5,(cols-1,righty),(0,lefty),(0,255,0),2)
cv.imshow('lightsrc_line',lightsrc5)


# **********
# 各种各样的形状拟合，在标识所识别到的对象时，肯定是由用的
# 以最小的误差来标识对象
# 不过肯定还有更多的实际意义，有待学习

cv.waitKey(0)

